Skip to main content

Advance Features

Decorators

A decorator is a special kind of declaration that can be attached to:

  • Classes
  • Methods
  • Properties
  • Parameters

They are functions that take metadata about the thing they decorate and optionally modify it.

You must enable them in tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true
}
}

General Syntax

A decorator is just a function, prefixed with @ when used:

function MyDecorator(target: any) {
console.log("Decorator called on:", target);
}

@MyDecorator
class Example {}

Class Decorators

Applied to a class declaration.

They receive the class constructor as their only argument.

function Logger(constructor: Function) {
console.log(`Class ${constructor.name} is created`);
}

@Logger
class Person {
constructor(public name: string) {}
}

Output when class is defined:

Class Person is created
  • @Logger runs when the class is declared, not when instantiated.
  • Can be used for logging, dependency injection, or modifying the class.

Modifying a Class

function Seal(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@Seal
class Car {
model = "Tesla";
}

Now the class and its prototype are sealed (no new properties can be added).

Method Decorators

Applied to a method inside a class.

They receive:

  1. The prototype of the class (or constructor for static methods).
  2. The method name.
  3. The property descriptor (can be modified).
function LogMethod(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with`, args);
return originalMethod.apply(this, args);
};
}

class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}

const calc = new Calculator();
console.log(calc.add(2, 3));

Output:

Calling add with [ 2, 3 ]
5
  • We intercepted the method call.
  • Useful for logging, caching, validation, performance measurement.

Property Decorators

Applied to class properties.

They can:

  • Observe or modify metadata about properties.
  • They cannot directly change property values (that happens at runtime).
function ReadOnly(target: any, propertyName: string) {
Object.defineProperty(target, propertyName, {
writable: false
});
}

class User {
@ReadOnly
role = "Admin";
}

const u = new User();
u.role = "User"; // ❌ Error in strict mode, role is read-only

Adding Metadata

function Format(formatString: string) {
return function (target: any, propertyName: string) {
let value: string;

const getter = () => value;
const setter = (newVal: string) => {
value = `${formatString} ${newVal}`;
};

Object.defineProperty(target, propertyName, {
get: getter,
set: setter
});
};
}

class Product {
@Format("SKU")
code: string = "";
}

const p = new Product();
p.code = "123";
console.log(p.code); // "SKU 123"

Parameter Decorators

Applied to method parameters. They let you observe metadata about parameters but cannot directly change their values.

function LogParameter(target: any, methodName: string, parameterIndex: number) {
console.log(`Parameter #${parameterIndex} in method ${methodName}`);
}

class Greeter {
greet(@LogParameter message: string) {
console.log("Hello,", message);
}
}

Output when class is defined

Parameter #0 in method greet

Summary of Decorators

Decorator TypeTargetUse Case
Class DecoratorClass constructorLogging, extending, dependency injection
Method DecoratorMethod descriptorLogging, security, caching
Property DecoratorClass propertyMetadata, validation, formatting
Parameter DecoratorMethod parameterMetadata for parameters (e.g., DI, validation)

Mixins

A Mixin is a way to combine reusable pieces of functionality into classes without using traditional inheritance (i.e., extends).

  • Unlike classical inheritance (single base class), mixins allow you to compose multiple behaviors into a single class.
  • They help achieve multiple inheritance–like behavior in TypeScript.

Think of them like "function helpers" that add behavior to classes."

Why Use Mixins?

  1. Code Reuse → Reuse functionality across multiple classes.
  2. Avoid Deep Inheritance → Instead of building tall class hierarchies, you "mix in" behavior.
  3. Composition over Inheritance → A common design principle.

How Mixins Work in TypeScript

TypeScript doesn’t directly support multiple inheritance (like in C++), but you can use mixins with a special pattern:

  1. Create classes (or traits) that define behaviors.
  2. Use a helper type (Constructor) to allow extending.
  3. Use Object.assign to mix functionality into the target class.

Example of Mixins

  1. Define a Constructor Type
type Constructor<T = {}> = new (...args: any[]) => T;

This is a generic constructor type — it allows mixins to accept any class as a base. 2. Create Mixins

// A mixin for adding logging capability
function Logger<TBase extends Constructor>(Base: TBase) {
return class extends Base {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
};
}

// A mixin for adding timestamp capability
function Timestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
getTimestamp() {
return new Date().toISOString();
}
};
}
  • Logger adds a .log() method.
  • Timestamp adds a .getTimestamp() method.
  • Both are reusable and can be applied to any class.
  1. Apply Mixins
// Base class
class Person {
constructor(public name: string) {}
}

// Apply mixins (Person -> Logger -> Timestamp)
class Employee extends Logger(Timestamp(Person)) {
constructor(name: string, public role: string) {
super(name);
}
}
  1. Use It
const emp = new Employee("Alice", "Developer");

console.log(emp.name); // Alice
console.log(emp.role); // Developer
emp.log("Started working!"); // [LOG]: Started working!
console.log(emp.getTimestamp()); // e.g. 2025-09-03T13:45:10.123Z

Employee class now inherits multiple behaviors via mixins.

Key Notes

  1. Mixins are composable → You can chain them in different orders.
  2. Order matters → The last mixin in the chain overrides previous ones if they define the same method.
  3. Better than multiple inheritance → Avoids diamond problem in C++.
  4. Helps code reuse → Without bloating single base classes.

keyof, typeof, and infer

keyof

keyof is a type operator that takes an object type and produces a union of its keys as string (or number) literal types.

type Person = {
name: string;
age: number;
isAdmin: boolean;
};

type PersonKeys = keyof Person;
// PersonKeys = "name" | "age" | "isAdmin"

So keyof extracts "name" | "age" | "isAdmin".

Usage with Generics

You can use it to ensure function parameters are valid property names:

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const person: Person = { name: "Alice", age: 25, isAdmin: true };

const name = getValue(person, "name"); // ✅ string
const age = getValue(person, "age"); // ✅ number
// getValue(person, "salary"); ❌ Error: "salary" is not a key of Person

keyof helps us enforce type safety when accessing object properties.

typeof

typeof in TypeScript has two meanings:

  1. Runtime JavaScript operator → Returns the type of a value as a string (e.g., "string", "number").
  2. TypeScript type operator → Gets the type of a value/variable so you can reuse it in type annotations.
let person = {
name: "Alice",
age: 25,
isAdmin: true,
};

type PersonType = typeof person;
// PersonType = { name: string; age: number; isAdmin: boolean }

infer

infer is a keyword used in conditional types that allows TypeScript to "infer" a type variable inside a type definition.

It’s often used when building utility types.

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // A = string
type B = UnwrapPromise<number>; // B = number
  • If T is a Promise<U>, then infer U.
  • Otherwise, just return T.

Example with Arrays:

type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean

infer lets us extract inner types from complex structures.

Template Literal Types

Template Literal Types in TypeScript are similar to JavaScript template strings (${...}), but they work at the type level.

They allow you to create new string literal types by combining unions, literals, or interpolating types into strings.

type Role = "admin" | "user" | "guest";

type RoleMessage = `Welcome, ${Role}`;
type RoleMessage = "Welcome, admin" | "Welcome, user" | "Welcome, guest";

String Manipulation with Built-in Helpers

TypeScript has utility types that work with template literals: Uppercase<T>, Lowercase<T>, Capitalize<T>, Uncapitalize<T>

type Greeting = "hello";
type Shout = Uppercase<Greeting>; // "HELLO"
type Capital = Capitalize<Greeting>; // "Hello"

Indexed Access Types

An Indexed Access Type lets you retrieve the type of a specific property (or set of properties) from another type using the T[K] syntax.

👉 If you know how you access object properties at runtime with obj["key"], indexed access types do the same thing at the type level.

type Person = {
name: string;
age: number;
isAdmin: boolean;
};

type NameType = Person["name"]; // string
type AgeType = Person["age"]; // number
type AdminType = Person["isAdmin"]; // boolean

Union of Keys

You can pass multiple keys (a union) to extract multiple property types:

type PersonInfo = Person["name" | "age"];
// string | number

Using keyof with Indexed Access

You can use keyof to extract all property types from an object:

type PersonValues = Person[keyof Person];
// string | number | boolean